package gui.chatarea;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;

import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import controller.MessageController;

import ui.MessageObserver;
import ui.StateObserver;
import user.LocalUser;
import user.User;
import user.UsersListObserver;
import utils.Utils;
import file.TransferFile;
import file.TransferFileObserver;
import gui.userslist.UserTargetedListener;

public class MultiChatArea extends JTabbedPane implements UsersListObserver, MessageObserver, StateObserver, TransferFileObserver, OpenTabListener  {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	/* The Main Room identifier & title. */
	private static final String MAIN_IDENTIFIER = "main" ;
	private static final String MAIN_TITLE = "Salon Principal" ;
	/* Color for tab title... */
	private static final Color NEW_MESSAGE_TAB_COLOR = Color.GREEN ;
	private static final Color NORMAL_TAB_COLOR = Color.CYAN ;
	private static final Color NEW_FILE_COLOR = Color.ORANGE ;
	private static final Color OFFLINE_COLOR = Color.RED ;
	
	/**
	 * Create a unique identifier according to sender and receivers information
	 * 
	 * @param sender
	 * @param receivers
	 * @return
	 */
	private static String createIdentifier (Map <String, User> users) {
		if (users == null || users.isEmpty()) {
			return  MAIN_IDENTIFIER;
		}
		else {
			ArrayList <String> address = new ArrayList <String> (users.keySet()) ; 
			Collections.sort(address) ;
			return Utils.join(address, "") ;
		}
	}
	
	/**
	 * Create a tab title from users informations
	 * 
	 * @param sender
	 * @param users
	 * @return
	 */
	private static String createTitle (Map <String, User> users) {
		if (users == null || users.isEmpty()) {
			return MAIN_TITLE ;
		}
		else if (users.size() == 1 && LocalUser.getLocalUser().equals(users.get(users.keySet().iterator().next()))) {
			return LocalUser.getLocalUser().getUsername() ;
		}
		else {
			ArrayList <String> usernames = new ArrayList <String> () ;
			for (String s : users.keySet()) {
				if (!LocalUser.getLocalUser().equals(users.get(s))) {
					usernames.add(users.get(s).getUsername()) ; 
				}
			}
			return Utils.join(usernames, ", ") ;
		}
	}
	
	private class ChatRoomChangedListener implements ChangeListener {
		@Override
		public void stateChanged(ChangeEvent ce) {
			if (getComponentCount() > 0 && getSelectedIndex() >= 0 && MultiChatArea.this.userTgtLst != null) { 
				ChatArea area = (ChatArea) getSelectedComponent() ;
				if (MultiChatArea.this.tabUsers.containsKey(area)) {
					Set <String> userIdentifiers = MultiChatArea.this.tabUsers.get(area).keySet() ;
					MultiChatArea.this.userTgtLst.usersTargeted(userIdentifiers) ;
					if (!userIdentifiers.isEmpty()) {
						/* If empty, mean that this tab correspond to a offline user ! */
						setBackgroundAt(getSelectedIndex(), NORMAL_TAB_COLOR) ;
					}

				}
			}
		}
	}

	private MessageController msgCtrl ;
	private UserTargetedListener userTgtLst ;
	
	/* TreeMap that link a conversation identifier with a tab Component. */
	private TreeMap <String, ChatArea> tabIdentifiers ;
	/* TreeMap that link a ChatArea with a list of users. */
	private IdentityHashMap <ChatArea, HashMap <String, User>> tabUsers ;
	/* TreeMap that link a user with it's color. */
	private TreeMap <String, String> userColors ;
	
	public MultiChatArea (MessageController msgCtrl) {
		super () ;
		this.msgCtrl = msgCtrl ;
		this.tabIdentifiers = new TreeMap <String, ChatArea> () ;
		this.tabUsers = new IdentityHashMap <ChatArea, HashMap <String, User>> () ;
		this.userColors = new TreeMap <String, String> () ;
		addChangeListener(new ChatRoomChangedListener()) ;
	}
	
	/**
	 * @param utl
	 */
	public void addUserTargetedListener (UserTargetedListener utl) {
		this.userTgtLst = utl ;
	}
	
	/**
	 * Try to get a ChatArea from the list. If it does not exists, create and return a new chat area.
	 * 
	 * @param from The sender (can be null if to is also null)
	 * @param to The receiver 
	 * @return
	 */
	private synchronized ChatArea getChatArea (Map <String, User> users) {
		String id = createIdentifier(users) ;
		ChatArea area = null ;
		if (this.tabIdentifiers.containsKey(id)) {
			area = this.tabIdentifiers.get(id) ;
		}
		else {
			/* Create the chat area */
			area = new ChatArea(this.userTgtLst, this.msgCtrl, this.userColors) ;
			this.tabIdentifiers.put(id, area) ;
			if (users != null) {
				this.tabUsers.put(area, new HashMap <String, User> (users)) ;
			}
			else {
				this.tabUsers.put(area, new HashMap <String, User> ()) ;
			}
			
		}
		return area ;
	}
	
	/**
	 * Try to get a ChatArea from this TabbedPane corresponding to specified TransferFile.
	 * 
	 * @param tf The specified TransferFile
	 * @return
	 */
	private synchronized ChatArea getChatArea (TransferFile tf) {
		Map <String, User> users = new HashMap <String, User> () ;
		users.put(tf.getReceiver().getIdentifier(), tf.getReceiver()) ;
		users.put(tf.getSender().getIdentifier(), tf.getSender()) ;
		return getChatArea (users) ;
	}
	
	/**
	 * @return The Main chat area
	 */
	private ChatArea getMainChatArea () {
		return getChatArea ((Map <String, User>)null) ;
	}
	
	/**
	 * Add the specified ChatArea to the TabbedPane if it is not already shown.
	 * 
	 * @param area
	 * @param title
	 */
	private synchronized void showChatArea (ChatArea area) {
		/* Si le composant n'est pas visible, on l'ajoute. */
		if (indexOfComponent(area) == -1) {
			addTab(createTitle(this.tabUsers.get(area)), area) ;
			try {
				setTabComponentAt(indexOfComponent(area), new ChatTabComponent(this)) ;
			} 
			catch (Exception e) { e.printStackTrace() ; }
		}
	}
	
	/**
	 * Find a user corresponding to user in all the Set of users and replace (or remove) it with user .
	 * This function also change the title and color (if remove) of all the tabulation containing 
	 * user if there are shown.
	 * 
	 * @param user The user to replace / remove
	 * @param remove true if the user should be removed
	 */
	private synchronized void updateUserSetAndTab (User user, boolean remove) {
		for (ChatArea area : this.tabUsers.keySet()) {
			Map <String, User> users = this.tabUsers.get(area) ;
			if (users.containsKey(user.getIdentifier())) {
				if (!remove || this.tabUsers.get(area).size() > 1) {
					this.tabUsers.get(area).remove(user.getIdentifier()) ;
				}
				if (!remove) {
					this.tabUsers.get(area).put(user.getIdentifier(), user) ;
				}
				if (isShown(area)) {
					setTitleAt(indexOfComponent(area), createTitle(this.tabUsers.get(area))) ;
					if (remove) {
						setBackgroundAt(indexOfComponent(area), OFFLINE_COLOR) ;
					}
				}
			}
		}
	}
	
	/**
	 * Update the tab title color
	 * 
	 * @param area
	 * @param co
	 */
	private synchronized void updateTabColor (ChatArea area, Color co) {
		setBackgroundAt(indexOfComponent(area), co) ;
	}
	
	/**
	 * @param area
	 * @return true if the selected area is in the selected tab
	 */
	private boolean isSelected (ChatArea area) {
		return getSelectedComponent() == area ;
	}
	
	/**
	 * @param area
	 * @return true if the selected area is shown in the tab pane
	 */
	private boolean isShown (ChatArea area) {
		return indexOfComponent(area) >= 0 ;
	}	
	
	private Map <String, User> createUserMap (User sender, ArrayList <User> receivers) {
		Map <String, User> users = new HashMap <String, User> () ;
		if (receivers != null) {
			for (User u : receivers) {
				users.put(u.getIdentifier(), u) ;
			}
			users.put(sender.getIdentifier(), sender) ;
			users.put(LocalUser.getLocalUser().getIdentifier(), LocalUser.getLocalUser()) ;
		}
		return users ;
	}

	@Override
	public void openTab(Set<User> users) {
		ChatArea area = null ;
		if (users != null) {
			area = getChatArea(createUserMap(LocalUser.getLocalUser(), new ArrayList <User> (users))) ;
		}
		else {
			area = getMainChatArea() ;
		}
		showChatArea(area) ;
		setSelectedComponent(area) ;
	}
	
	@Override
	public void closeSelectedTab() {
		if (getSelectedIndex() != -1) {
			remove(getSelectedIndex()) ;
		}
	}
	
	private static int[] DEFAULT_COLORS = {
		0xff0000, 0x00ff00, 0x0000ff, 0xeeee55, 0xff00ff, 0x55ff55
	} ;
	private int currentColorIndex = (Math.abs(((new Random(System.currentTimeMillis())).nextInt()))) % DEFAULT_COLORS.length;
	
	private String createNewColor () {
		int r = (DEFAULT_COLORS[currentColorIndex] >> 16) % 256;
		int g = (DEFAULT_COLORS[currentColorIndex] >> 8) % 256;
		int b = (DEFAULT_COLORS[currentColorIndex]) % 256;
		currentColorIndex = (currentColorIndex + 1) % DEFAULT_COLORS.length ;
		return String.format("#%02x%02x%02x", r, g, b) ;
	}
	
	@Override
	public void fireNewUser(User u) {
		if (!this.userColors.containsKey(u.getIdentifier())) {
			this.userColors.put(u.getIdentifier(),
				createNewColor()) ;
		}
		ChatArea area = getMainChatArea() ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			doc.printNewUser(u) ;
		}
		updateUserSetAndTab(u, false) ;
		if (isShown(area)) {
			updateTabColor(area, NORMAL_TAB_COLOR) ;
		}
	}

	@Override
	public void fireUpdateUser(User oldUser, User newUser) {
		HTMLChatDocument doc = getMainChatArea().getDocument() ;
		synchronized (doc) {
			doc.printUpdateUser(oldUser, newUser) ;
		}
		updateUserSetAndTab(newUser, false) ;
	}

	@Override
	public void fireDeleteUser(User u) {
		if (this.msgCtrl.connected()) {
			HTMLChatDocument doc = getMainChatArea().getDocument() ;
			synchronized (doc) {
				doc.printRemoveUser(u) ;
			}
		}
		updateUserSetAndTab(u, true) ;
	}
	
	
	/**
	 * Show the CHatArea if it is not already shown and update it's tab color
	 * if it is not selected.
	 * 
	 * @param area
	 * @param co
	 */
	private void showAndChangeColor (ChatArea area, Color co) {
		if (!isShown(area)) {
			showChatArea(area) ;
		}
		if (!isSelected(area)) {
			updateTabColor(area, co) ;
		}
	}
	

	@Override
	public void fireNewMessage(User from, ArrayList <User> to, String text) {
		ChatArea area = getChatArea (createUserMap(from, to)) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			doc.printMessage(from, text) ;	
		}
		showAndChangeColor (area, NEW_MESSAGE_TAB_COLOR) ;
		if (LocalUser.getLocalUser().equals(from)) {
			setSelectedComponent(area) ;
		}
	}
	
	
	@Override
	public void fireLocalFileQuery (TransferFile tf) {
		ChatArea area = getChatArea(tf) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			doc.printAskFile(tf) ;
		}
		showAndChangeColor (area, NEW_FILE_COLOR) ;
	}

	@Override
	public void fireFileResponse (TransferFile tf, boolean accepted) {
		ChatArea area = getChatArea(tf) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			if (accepted) {
				doc.printFileAccepted(tf) ;
			}
			else {
				doc.printFileRefused(tf) ;
			}
		}
		showAndChangeColor (area, NEW_FILE_COLOR) ;
	}

	@Override
	public void fireRemoteFileQuery (TransferFile tf) {
		ChatArea area = getChatArea(tf) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			doc.printSaveFile(tf) ;
		}
		showAndChangeColor (area, NEW_FILE_COLOR) ;
	}

	@Override
	public void startFileTransfer(TransferFile tf) {
		ChatArea area = getChatArea(tf) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			doc.printFileTransferStarted(tf) ;
		}
		showAndChangeColor (area, NEW_FILE_COLOR) ;
	}

	@Override
	public void updateFileTransfer(TransferFile tf, int transfer) {
		ChatArea area = getChatArea(tf) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			doc.printFileTransferUpdated(tf, transfer) ;
		}
		showAndChangeColor (area, NEW_FILE_COLOR) ;
	}

	@Override
	public void stopFileTransfer(TransferFile tf, boolean correct) {
		ChatArea area = getChatArea(tf) ;
		HTMLChatDocument doc = area.getDocument() ;
		synchronized (doc) {
			if (correct) {
				doc.printFileTransferSucceed(tf) ;
			}
			else {
				doc.printFileTransferFailed(tf) ;
			}
		}
		showAndChangeColor (area, NEW_FILE_COLOR) ;
	}

	@Override
	public void fireConnect() {
		showChatArea(getMainChatArea()) ;
	}

	@Override
	public void fireDisconnect() {		
	}

}
